// Author: wsfuyibing <682805@qq.com>
// Date: 2025-04-10

package database

import (
	"context"
	"gitee.com/go-libs/config"
	"gitee.com/go-libs/log"
	"gitee.com/go-wares/framework-gin/framework/errs"
	"gorm.io/driver/mysql"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"sync"
)

var Mgr Manager

type (
	// Manager is database config manager component, It's initial values
	// scanned from one of following yaml files:
	//
	//   config/db.yml
	//   config/db.yaml
	Manager interface {
		GetConfig(key string) (config *Config, has bool)
		GetMasterConnection(ctx context.Context, keys ...string) (db *gorm.DB, err error)
		GetSlaverConnection(ctx context.Context, keys ...string) (db *gorm.DB, err error)
		SetConfig(key string, config *Config)
	}

	database struct {
		Databases map[string]*Config `yaml:"databases"`

		mu                *sync.RWMutex
		masterConnections map[string]*gorm.DB
		slaverConnections map[string]*gorm.DB
	}
)

func (o *database) After() {
	for _, v := range o.Databases {
		if v != nil {
			v.Defaulter()
		}
	}
}

func (o *database) GetConfig(key string) (config *Config, has bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	config, has = o.Databases[key]
	return
}

func (o *database) GetMasterConnection(ctx context.Context, keys ...string) (db *gorm.DB, err error) {
	var (
		key = o.key(keys...)
		ok  bool
		v   *gorm.DB
	)

	o.mu.Lock()
	defer o.mu.Unlock()

	// 1. Read from cached and open session.
	if v, ok = o.masterConnections[key]; ok {
		db = o.open(ctx, v)
		return
	}

	// 2. Build with given key.
	if v, err = o.build(ctx, key, true); err != nil {
		log.Errorfc(ctx, `connection build: error="%v"`, err)
		return
	}

	// 3. Update caches.
	o.masterConnections[key] = v

	// 4. Open connection session.
	db = o.open(ctx, v)
	return
}

func (o *database) GetSlaverConnection(ctx context.Context, keys ...string) (db *gorm.DB, err error) {
	var (
		key = o.key(keys...)
		ok  bool
		v   *gorm.DB
	)

	o.mu.Lock()
	defer o.mu.Unlock()

	// 1. Read from cached and open session.
	if v, ok = o.slaverConnections[key]; ok {
		db = o.open(ctx, v)
		return
	}

	// 2. Build with given key.
	if v, err = o.build(ctx, key, false); err != nil {
		log.Errorfc(ctx, `connection build: error="%v"`, err)
		return
	}

	// 3. Update caches.
	o.slaverConnections[key] = v

	// 4. Open connection session.
	db = o.open(ctx, v)
	return
}

func (o *database) SetConfig(key string, config *Config) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	if _, ok := o.Databases[key]; !ok {
		o.Databases[key] = config
	}
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *database) build(ctx context.Context, key string, primary bool) (db *gorm.DB, err error) {
	var (
		cfg *Config
		dia func(string) gorm.Dialector
		dsn string
		has bool
	)

	// 1. Read config with key.
	if cfg, has = o.Databases[key]; !has {
		err = errs.ErrDatabaseKeyUndefined
		return
	}

	// 2. At least 1 dsn.
	if cfg.DsnCount == 0 {
		err = errs.ErrDatabaseDsnUndefined
		return
	}

	// 3. Check driver.
	switch cfg.Driver {
	case "mysql":
		dia = mysql.Open
	case "postgres":
		dia = postgres.Open
	default:
		err = errs.ErrDatabaseUnsupportedDriver
		return
	}

	// 4. Check dsn.
	if primary || cfg.DsnCount == 1 {
		dsn = cfg.Dsn[0]
	} else {
		dsn = cfg.Dsn[1]
	}

	// 5. Create connection.
	if db, err = gorm.Open(dia(dsn), &gorm.Config{}); err == nil {
		log.Debugfc(ctx, `connection built: driver="%s", key="%v"`, cfg.Driver, key)
		db = db.Session(&gorm.Session{Logger: (&logging{cfg: cfg, key: key}).init()})
	}
	return
}

func (o *database) init() *database {
	o.mu = new(sync.RWMutex)
	o.masterConnections = make(map[string]*gorm.DB)
	o.slaverConnections = make(map[string]*gorm.DB)

	_ = config.Seek("db.yaml", "db.yml").ScanYaml(o)
	return o
}

func (o *database) key(keys ...string) string {
	if len(keys) > 0 && keys[0] != "" {
		return keys[0]
	}
	return defaultKey
}

func (o *database) open(ctx context.Context, v *gorm.DB) *gorm.DB {
	return v.WithContext(ctx)
}
